Redd's Art [rev]

solved by not_really

Writeup by not_really

Redd has an enticing deal for you. Will you take it?

Author: 2much4u

Files: ReddsArt

Time to open up Ghidra.


No debug info, and of course stupid Ghidra can’t figure out what the main function is unlike IDA. (It’s the last one)

undefined8 FUN_00100bea(void) {
  size_t sVar1;
  long in_FS_OFFSET;
  char local_32 [10];
  char local_28 [24];
  long local_10;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_00100b16("Well, well! You from around here?\n");
  FUN_00100b16("Hi, the name\'s Redd. I work in sales.\n");
  FUN_00100b16("And you are... \n");
  FUN_00100b16("[name] ");
  FUN_00100b16("! What a great name! Intelligent. Strong.\n");
  FUN_00100b16("I can already tell we\'re gonna be pals.\n");
  FUN_00100b16("No, not pals...family!\n\n");
  FUN_00100b16("... It\'s a pleasure to meet ya, ");
  FUN_00100b16("How does 133,337 Bells grab ya?\n");
  FUN_00100b16("It\'s a bargain. Whaddaya say?\n");
  FUN_00100b16("[yes/no] ");
  sVar1 = strlen(local_32);
  if (sVar1 == 3) {
  } else {
    FUN_00100b16("For only 1 Bell, you\'ll be the proud owner of your very own famous painting.\n");
    FUN_00100b16("Whaddaya say?\n");
    FUN_00100b16("[yes/no] ");
    sVar1 = strlen(local_32);
    if (sVar1 == 3) {
    else {
      FUN_00100b16("You do drive a hard bargain, ");
      FUN_00100b16(".\nI\'ll tell you what.\n");
      FUN_00100b16("Since I trust you so much...\n");
      FUN_00100b16("This one is on me!\n");
      FUN_00100b16("You won\'t get this deal next time!\n\n");
  //stack check
  return 0;

FUN_00100b16 is probably print so let’s rename that.

The only other called function in main is FUN_00100b82.

void FUN_00100b82(void) {
  printSlow("\nHa! You\'re NOT gonna regret this!\n");
  printSlow("I\'ll just take those bells from you...\n");
  printSlow("I knew you had an eye for art!\n");
  printSlow("It\'s like it was meant to be.\n");
  printSlow("I hope we can do this again!\n\n");

They all seem to print uiuctf{v3Ry_r341_@rTT} which is not the real flag.

Here’s the other unknown functions:

ulong FUN_0010091a(void) {
  size_t sVar1;
  uint local_20;
  int local_1c;
  local_20 = 0;
  local_1c = 0;
  while(true) {
    sVar1 = strlen(PTR_s_uiuctf{v3Ry_r341_@rTT}_00302010);
    if (sVar1 <= (ulong)(long)local_1c) break;
    local_20 += (int)(char)PTR_s_uiuctf{v3Ry_r341_@rTT}_00302010[local_1c];
  return (ulong)local_20;

undefined * FUN_00100abb(void) {
  return PTR_s_#?FJdDpTtbkE{fWyeAD:,yhO}yen)Z_00302018;

undefined * FUN_00100ac8(void) {
  return PTR_s_mjQeso),~lhuiYB-okg>ZkM.sQ,_-c_00302020;

undefined * FUN_00100ad5(void) {
  return PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028;

undefined * FUN_00100ae2(void) {
  return PTR_s_LVTQtggnGJO:'W$<Nf`mU;iRHe~SZU_00302030;

undefined * FUN_00100aef(void) {
  return PTR_s_+XAM$Dmv!bevK*dcPoGo`a;QX]cq>O_00302038;

undefined * FUN_00100afc(void) {
  return PTR_s_~S*l(bA[r~akpghl{v[/]Q*vbC|z->_00302040;
undefined * FUN_00100b09(void) {
  return PTR_s_fJCwLEzyI'C=KXdNN,MtJ:h/gT^b(c_00302048;

Looks like there’s some encrypted flags maybe? The only interesting thing here is FUN_0010091a which might be some decryption code. But there’s no flag data to be found in this function, we’re only reading that fake flag. Well, if we look at the xrefs for that function we see that there is another function we Ghidra didn’t pick up at 00100a67.


Press f on it to convert it to a function and decompile it.

void FUN_00100a5a(void) {
  byte bVar1;
  int local_18;
  bVar1 = FUN_0010091a();
  local_18 = 0;
  while (local_18 < 0xe7) {
    (&DAT_00100973)[local_18] = (&DAT_00100973)[local_18] ^ bVar1;

Now we have some xoring, and it seems to use FUN_0010091a as an xor key. It also seems to be decrypting DAT_00100973.


Interesting… 🤔

Let’s try running the decryption code (ported to python).

def fun0010091a():
    local_20 = 0
    local_1c = 0
    while True:
        sVar1 = len("uiuctf{v3Ry_r341_@rTT}")
        if sVar1 <= local_1c:
        local_20 += ord("uiuctf{v3Ry_r341_@rTT}"[local_1c])
        local_1c += 1
    return local_20

def getDat00100973():
    with open("ReddsArt", "rb") as asm:
        asm.seek(0x973) # assembly has a base of 0x100000, the actual file pos is 0x973
        data = asm.read(0xe7)
    return data

def fun00100a5a():
    bVar1 = fun0010091a() & 0xff # b in bVar1 means byte so we and to one byte
    encText = bytearray(getDat00100973())
    local_18 = 0
    while local_18 < 0xe7:
        encText[local_18] = encText[local_18] ^ bVar1
        local_18 += 1



It’s not text, but we’re close. A normal person might not recognize this, but I can tell by the 55 48 89 e5 that this is an assembly function. Since it was simple xor, the unencrypted assembly bytes can fit where the encrypted bytes were, so let’s just overwrite them in ReddsArt.


Then reload the assembly in Ghidra and go back to 0x00100973.

void FUN_00100973(void) {
  char cVar1;
  byte bVar2;
  size_t sVar3;
  int local_2c;
  int local_28;
  cVar1 = *(char *)((long)DAT_00000009 + 9);
  local_2c = 0;
  while(true) {
    sVar3 = strlen(PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028);
    if (sVar3 <= (ulong)(long)local_2c) break;
    PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028[local_2c] = PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028[local_2c] + cVar1;
    local_2c = local_2c + 1;
  bVar2 = FUN_0010091a();
  local_28 = 0;
  while(true) {
    sVar3 = strlen(PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028);
    if (sVar3 <= (ulong)(long)local_28) break;
    PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028[local_28] = PTR_s_hthzgubI>*ww7>z+Ha,m>W,7z+hmG`_00302028[local_28] ^ bVar2;
    local_28 = local_28 + 1;

Should be as simple as rewriting this code too, right? But wait, what’s cVar1? If you try to double click it, it says it doesn’t exist in ram. We could debug, or because it’s a char, we can just loop over all 256 combos.

def fun00100973(cVar1):
    local_2c = 0
    encFlag = bytearray(b"hthzgubI>*ww7>z+Ha,m>W,7z+hmG`")
    while True:
        sVar3 = len(encFlag)
        if sVar3 <= local_2c:
        encFlag[local_2c] += cVar1
        local_2c += 1
    bVar2 = fun0010091a() & 0xff
    local_28 = 0
    while True:
        sVar3 = len(encFlag)
        if sVar3 <= local_28:
        encFlag[local_28] = encFlag[local_28] ^ bVar2;
        local_28 += 1
    print(f"cVar1={cVar1} - {encFlag.decode('latin-1')}")

for i in range(255):
cVar1=0 - vjvdyk|W 4ii) d5V2s I2)d5vsY~
cVar1=1 - wkwevh}T!5ff&!e2W|3p!F3&e2wpV
cVar1=2 - thtbwizU^2gg'^b3T}0q^G0'b3tqW|
cVar1=3 - uiuctf{R_3dd$_c0Uz1n_D1$c0unT}

The assembly wasn’t that hard to convert to python because it was so short, but what if it was really long? Well, we could change the assembly to call these functions.

Well, we can patch instructions to call the decryption and flag code ourselves. We could use Ghidra’s patch instruction option, but it always seems to corrupt the assembly for me. So I’ll just manually hex edit (if complex enough, I just use radare2).


Now let’s run the program.


Uh oh, what is it this time?

Well, you probably could’ve guessed but because the code is in .text, we don’t have access to writing there.


I didn’t try to go any further with this. Sadly, it’s not as easy as just setting the writable flag to true (tried that). According to the interwebs, we would need to patch in some instructions to call mprotect.